"""Security headers middleware for Flask applications.

This module provides comprehensive HTTP security headers to protect against
common web vulnerabilities identified by OWASP ZAP and other security scanners.
"""

from typing import Optional

from flask import Flask, Response, request
from loguru import logger


class SecurityHeaders:
    """Configure and apply security headers to Flask responses.

    Addresses security vulnerabilities:
    - CSP: Content Security Policy to prevent XSS
    - Clickjacking: X-Frame-Options protection
    - MIME sniffing: X-Content-Type-Options
    - Spectre: Cross-Origin policies
    - Feature abuse: Permissions-Policy
    - Information leakage: Server header removal
    - Protocol downgrade: HSTS (Strict-Transport-Security)
    """

    def __init__(self, app: Optional[Flask] = None) -> None:
        """Initialize security headers middleware.

        Args:
            app (Optional[Flask]): Flask application instance (optional, can call init_app later)
        """
        if app is not None:
            self.init_app(app)

    def init_app(self, app: Flask):
        """Initialize the Flask application with security headers.

        Args:
            app: Flask application instance

        Security Configuration:
            SECURITY_CSP_CONNECT_SRC: Restricts WebSocket/fetch connections (default: 'self' ws: wss:)
            SECURITY_CORS_ENABLED: Enable CORS for API routes (default: True)
            SECURITY_CORS_ALLOW_CREDENTIALS: Allow credentials in CORS (default: False)
            SECURITY_CORS_ALLOWED_ORIGINS: Allowed CORS origins, comma-separated for multiple
                                           (default: "" - requires explicit configuration).
                                           Multi-origin uses origin reflection.
            SECURITY_COEP_POLICY: Cross-Origin-Embedder-Policy applied globally to ALL routes
                                  (default: "credentialless"). Options: require-corp, credentialless, unsafe-none

        Important Security Trade-offs:
            - CSP includes 'unsafe-inline' for Socket.IO compatibility
            - This reduces XSS protection but is necessary for real-time WebSocket functionality
            - 'unsafe-eval' has been removed to prevent eval() XSS attacks
            - If Socket.IO is removed, 'unsafe-inline' should also be tightened immediately
            - Monitor CSP violation reports to detect potential XSS attempts
            - COEP policy is applied globally, not per-route. 'credentialless' allows cross-origin
              requests without credentials. Use 'require-corp' for stricter isolation.
        """
        # Set default security configuration
        # connect-src uses 'self' + ws:/wss: to work from any origin (localhost, LAN, Docker, internet)
        app.config.setdefault(
            "SECURITY_CSP_CONNECT_SRC",
            "'self' ws: wss:",
        )
        app.config.setdefault("SECURITY_CORS_ENABLED", True)
        app.config.setdefault("SECURITY_CORS_ALLOW_CREDENTIALS", False)
        # Default to empty (fail closed) - require explicit configuration for CORS
        # Using "*" creates a security footgun - users must explicitly allow origins
        app.config.setdefault("SECURITY_CORS_ALLOWED_ORIGINS", "")
        app.config.setdefault("SECURITY_COEP_POLICY", "credentialless")

        self.app = app
        app.after_request(self.add_security_headers)

        # Validate CORS configuration at startup
        self._validate_cors_config()

        logger.info("Security headers middleware initialized")
        logger.warning(
            "CSP configured with 'unsafe-inline' for Socket.IO compatibility. "
            "'unsafe-eval' removed for better XSS protection. Monitor for CSP violations."
        )

    def get_csp_policy(self) -> str:
        """Generate Content Security Policy header value.

        Returns:
            CSP policy string with directives for Socket.IO compatibility

        Note:
            - 'unsafe-inline' is required for Socket.IO compatibility
            - 'unsafe-eval' removed for better XSS protection (not needed for Socket.IO)
            - connect-src uses 'self' ws: wss: by default (works from any origin)
        """
        connect_src = self.app.config.get("SECURITY_CSP_CONNECT_SRC", "'self'")
        return (
            "default-src 'self'; "
            f"connect-src {connect_src}; "
            "script-src 'self' 'unsafe-inline'; "
            "style-src 'self' 'unsafe-inline'; "
            "font-src 'self' data:; "
            "img-src 'self' data:; "
            "worker-src blob:; "
            "frame-src 'self'; "
            "object-src 'none'; "
            "base-uri 'self'; "
            "form-action 'self';"
        )

    @staticmethod
    def get_permissions_policy() -> str:
        """Generate Permissions-Policy header value.

        Disables potentially dangerous browser features by default.

        Returns:
            Permissions-Policy string
        """
        return (
            "geolocation=(), "
            "midi=(), "
            "camera=(), "
            "usb=(), "
            "magnetometer=(), "
            "accelerometer=(), "
            "gyroscope=(), "
            "microphone=(), "
            "payment=(), "
            "sync-xhr=(), "
            "document-domain=()"
        )

    def add_security_headers(self, response: Response) -> Response:
        """Add comprehensive security headers to Flask response.

        Args:
            response: Flask response object

        Returns:
            Response object with security headers added

        Note:
            Cross-Origin-Embedder-Policy (COEP) is applied globally to all routes.
            Default is 'credentialless' which allows cross-origin requests without
            credentials. If stricter isolation is needed, configure SECURITY_COEP_POLICY
            to 'require-corp', but this may break cross-origin API access.
        """
        # Content Security Policy - prevents XSS and injection attacks
        csp = self.get_csp_policy()
        response.headers["Content-Security-Policy"] = csp

        # Anti-clickjacking protection
        response.headers["X-Frame-Options"] = "SAMEORIGIN"

        # Prevent MIME-type sniffing
        response.headers["X-Content-Type-Options"] = "nosniff"

        # Spectre vulnerability mitigation - applied globally
        response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
        coep_policy = self.app.config.get(
            "SECURITY_COEP_POLICY", "credentialless"
        )
        response.headers["Cross-Origin-Embedder-Policy"] = coep_policy
        response.headers["Cross-Origin-Resource-Policy"] = "same-origin"

        # Permissions Policy - disable dangerous features
        response.headers["Permissions-Policy"] = self.get_permissions_policy()

        # Remove server version information to prevent information disclosure
        response.headers.pop("Server", None)

        # Referrer Policy - control referrer information
        response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"

        # HSTS - enforce HTTPS connections (only when request is secure)
        # request.is_secure checks wsgi.url_scheme == 'https'
        # ProxyFix (configured in app_factory) handles X-Forwarded-Proto for reverse proxies
        if request.is_secure:
            response.headers["Strict-Transport-Security"] = (
                "max-age=31536000; includeSubDomains"
            )

        # Add CORS headers for API requests if enabled
        if self._is_api_route(request.path) and self.app.config.get(
            "SECURITY_CORS_ENABLED", True
        ):
            response = self._add_cors_headers(response)

        return response

    @staticmethod
    def _is_api_route(path: str) -> bool:
        """Check if the request path is an API route.

        Args:
            path: Request path to check

        Returns:
            True if path is an API route
        """
        return path.startswith("/api/") or path.startswith("/research/api/")

    def _validate_cors_config(self) -> None:
        """Validate CORS configuration at startup to catch misconfigurations early.

        Raises:
            ValueError: If CORS configuration is invalid
        """
        if not self.app.config.get("SECURITY_CORS_ENABLED", True):
            return

        allowed_origins = self.app.config.get(
            "SECURITY_CORS_ALLOWED_ORIGINS", ""
        )
        allow_credentials = self.app.config.get(
            "SECURITY_CORS_ALLOW_CREDENTIALS", False
        )

        # Validate credentials with wildcard origin
        if allow_credentials and allowed_origins == "*":
            raise ValueError(
                "CORS misconfiguration: Cannot use credentials with wildcard origin '*'. "
                "Set SECURITY_CORS_ALLOWED_ORIGINS to a specific origin or disable credentials."
            )

        # Log info about multi-origin + credentials configuration
        if allow_credentials and "," in allowed_origins:
            logger.info(
                f"CORS configured with multiple origins and credentials enabled. "
                f"Origins: {allowed_origins}. "
                f"Using origin reflection pattern for security."
            )

    def _add_cors_headers(self, response: Response) -> Response:
        """Add CORS headers for API routes using origin reflection for multi-origin support.

        Args:
            response: Flask response object to modify

        Returns:
            Response object with CORS headers added

        Security Note:
            - Uses "origin reflection" pattern for multi-origin support (comma-separated origins config)
            - Reflects the requesting origin back if it matches the whitelist
            - Wildcard origin (*) disables credentials per CORS spec
            - Single origin or reflected origins allow credentials if configured
        """
        configured_origins = self.app.config.get(
            "SECURITY_CORS_ALLOWED_ORIGINS", ""
        )
        allow_credentials = self.app.config.get(
            "SECURITY_CORS_ALLOW_CREDENTIALS", False
        )

        # Determine which origin to send back
        origin_to_send = configured_origins
        request_origin = request.headers.get("Origin")

        # If wildcard, allow all origins
        if configured_origins == "*":
            origin_to_send = "*"
        # If multiple origins configured (comma-separated), use origin reflection
        elif "," in configured_origins:
            # Parse configured origins into a set
            allowed_origins_set = {
                origin.strip() for origin in configured_origins.split(",")
            }

            # Reflect the request origin if it's in the whitelist (when present)
            if request_origin:
                if request_origin in allowed_origins_set:
                    origin_to_send = request_origin
                else:
                    # Request origin not in whitelist - log but still set configured origin
                    # (Browsers will enforce CORS, this just logs for monitoring)
                    logger.warning(
                        f"CORS request from non-whitelisted origin: {request_origin}. "
                        f"Allowed origins: {configured_origins}"
                    )
                    # Use first configured origin for backward compatibility
                    origin_to_send = list(allowed_origins_set)[0]
        # Single origin configured - always use it (browser enforces CORS)
        else:
            origin_to_send = configured_origins

        response.headers["Access-Control-Allow-Origin"] = origin_to_send
        response.headers["Access-Control-Allow-Methods"] = (
            "GET, POST, PUT, DELETE, OPTIONS"
        )
        response.headers["Access-Control-Allow-Headers"] = (
            "Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override"
        )

        # Set credentials if configured and not using wildcard
        # Note: Startup validation ensures credentials is False with wildcard or multi-origin
        if allow_credentials and origin_to_send != "*":
            response.headers["Access-Control-Allow-Credentials"] = "true"

        response.headers["Access-Control-Max-Age"] = "3600"

        return response
